/*
 * Copyright 2010
 * Jairou Software Organization
 * All Rights Reserved.
 * 
 * This software is the proprietary 
 * information of Roger Ramia
 * and the Jairou Software Organization 
 * Use is subject to license terms.
 * 
 * For licensing information please visit
 * http://www.jairou.org/license
 * 
 * @author Roger Ramia
 */
package org.jairou.core;

import java.lang.reflect.Method;
import java.util.StringTokenizer;
import java.util.Timer;
import java.util.TimerTask;


/**
 * Declarative function call support.
 * Provides the ability to invoke pre-configured
 * static functions from within an XML defined request						   
 *
 * @author Roger Ramia 
 */
public class Functions {
	
	/**
	 * uses reflection to execute an XML defined 
	 * static function call using the following syntax:
	 * <br/>
	 * '${functionName(param1, param2...)}'
	 * <br/>
	 * where '${' - starts the expression
	 *<br/>
	 * 'functionName' - is the predefined XML name within jairou.xml
	 * 			    	which is not necessarily the same as the 
	 * 					name of the method defined on the target class.
	 * <br/>
	 * '(param1, param2...)' - is the parameters list as
	 * 						   required by the function.  Literal
	 * 						   strings can be passed in given
	 * 						   they are wrapped by single quotes.
	 * 						   Additionally context based objects
	 * 						   can be passed provided an expession 
	 * 						   i.e. ${dot notation path}
	 * 						   to the context property.  For example: 
	 * 						   use ${session.user.firstName} to pass in 
	 * 						   the 'firstName' property of the current
	 * 						   'user' attribute stored in Session scope
	 * 
	 * 					       Note: If the underlying method accepts a 
	 * 						   java.lang.Object typed parameter, and nothing
	 * 						   is specified in the XML call for that parameter,
	 * 						   the current context will be passed in as the Object.  
	 * 						   This allows you to write methods that accept the
	 * 						   current context without needing to specify anything
	 * 						   in the argument list of the XML call.
	 * 	<br/>				
	 * '}' - ends the expression
	 *  
	 * @param call - String in the format: ${functionName(param1, param2...)}
	 * @param ctx - the current context used to obtain the runtime parameters
	 * @return object - the resulting output of the underlying method call
	 * @throws Exception - if the reflective invocation results in an exception
	 */
	public static Object call(String call, Object ctx) 
	throws Exception {
		Object out = null;
		if (isCall(call)) {			
			String function = call.substring(0, call.indexOf("("));
			String functionClassName = XML.get("function."+function+".class");
			String functionMethodSignature = XML.get("function."+function+".method");
			int contextIndex = 0;
			boolean contextUsed = false;
			if (functionClassName != null && 
				functionMethodSignature != null) {
				String methodName = functionMethodSignature.substring(0, functionMethodSignature.indexOf("("));
				String methodParams = functionMethodSignature.substring(functionMethodSignature.indexOf("(")+1, functionMethodSignature.lastIndexOf(")"));
				StringTokenizer methodParameters = new StringTokenizer(methodParams, ",");
				
				// load all classes into class array
				Class<?>[] methodParamsClassArray = new Class[methodParameters.countTokens()];
				int methodParamsCount = 0;
				while(methodParameters.hasMoreTokens()) {
					String currParam = methodParameters.nextToken().trim();
					if (currParam.equals("java.lang.Object")) {
						contextUsed = true;
						contextIndex = methodParamsCount;
					}
					Class currClass = Class.forName(currParam);
					methodParamsClassArray[methodParamsCount] = currClass;	
					methodParamsCount++;	
				}
							
				// load all objects into object array
				String params = call.substring(call.indexOf("(")+1, call.lastIndexOf(")"));
				StringTokenizer parameters = new StringTokenizer(params, ",");
				Object[] paramsArray = new Object[methodParamsCount];
				int paramsCount = 0;
				while(parameters.hasMoreTokens()) {
					String paramString = parameters.nextToken().trim();					
					if (paramsCount == contextIndex && contextUsed) paramsArray[paramsCount] = ctx;
					else {
						Object typedParam = paramString;
						if (paramString.startsWith("'") && paramString.endsWith("'"))
							typedParam = paramString.substring(1, paramString.length()-1);
						else if (paramString.startsWith("${") && paramString.endsWith("}")) {
							typedParam = Objects.find(paramString.substring(2, paramString.length()-1), ctx);
						}
						String className = methodParamsClassArray[paramsCount].getName();
						typedParam = Objects.load(className, typedParam);
						paramsArray[paramsCount] = typedParam;
					}
					paramsCount++;
				}
				if (paramsCount < methodParamsCount && contextUsed) {
					paramsArray[contextIndex] = ctx;
				}
								
				Object functionObj = Objects.newInstance(functionClassName);
				if (functionObj != null) {
					Method method = functionObj.getClass().getMethod(methodName, methodParamsClassArray);
					out = method.invoke(functionObj, paramsArray);
				}
			}
		}
		return out;
	}
	
	/**
	 * overloaded call which passes a null context
	 * @param call
	 * @return
	 * @throws Exception
	 */
	public static Object call(String call) 
	throws Exception {
		return call(call, null);
	}
	
	
	/**
	 * indicates if the string passed in
	 * is a jairou static function call
	 * 
	 * @param String in the format: ${functionName(param1, param2...)}
	 * @return boolean true if the call string has the correct syntax
	 */
	public static boolean isCall(String call) {
		return (call != null && call.indexOf("(") > 0 && call.endsWith(")"));
	}
	
	/**
	 * schedules an XML defined function call
	 * to run at a given start time and repeat
	 * on a specified interval.
	 * 
	 * {@code<task>} nodes are typically defined in jairou.xml 
	 * For example: 
	 * XML configuration of...	 
	 * <pre>{@code 		
	 * 	<timers>
	 * 		<task id="email" run="${js('sendEmails')}" start="60000" interval="60000"/>
	 * 	</timers>
	 * }</pre>
	 * 
	 * Will schedule a call to JS.invoke('processemails', ctx) to start in 60 seconds
	 * and repeat every 60 seconds until the runtime environment is shutdown.
	 * <br/>
	 * The timers node is read at server startup and all tasks defined are scheduled
	 * at that time.  
	 * <br/>
	 * Note: the only available scope at server startup is 'application'
	 * thus startup tasks can only read/write data to the application scope or must
	 * use some other persistence mechanism (i.e. a database or file system).
	 * 
	 * @param prefix element name where the tasks are defined (usually 'timers') 
	 * 				   but can be any wrapping element provided the <task> elements
	 * 				   have the appropriate properties (id, run, start and interval)
	 * @throws Exception if underlying function call throws an exception
	 */
	public static void schedule(String prefix, Object context) 
	throws Exception {
		String[] tasks = XML.getIds(prefix+".task");
		if (tasks != null && tasks.length > 0) {
			for (final String id: tasks) {
				final String run = XML.get(prefix+".task."+id+".run");
				Long start = Long.parseLong(XML.get(prefix+".task."+id+".start"));
				Long interval = Long.parseLong(XML.get(prefix+".task."+id+".interval"));
				final Object ctx = context;
				if (id != null && run != null) {
					final Timer timer = new Timer(id, true);
					timer.scheduleAtFixedRate(new TimerTask(){
						public void run() {
							try {
								XML.resolveExpression(run, ctx);
							}
							catch(Exception e) {
								timer.cancel();
								e.printStackTrace();
								System.out.println("Error executing task:"+id+" "+e.getMessage());
							}
						}
					},start, interval);
				}
			}
		}
	}
}